package cn.bbstone.pisces2.client.task.impl;

import cn.bbstone.pisces2.client.base.ClientCache;
import cn.bbstone.pisces2.client.task.ITask;
import cn.bbstone.pisces2.client.task.TaskListener;
import cn.bbstone.pisces2.client.task.event.FailEvent;
import cn.bbstone.pisces2.client.task.event.FailEventData;
import cn.bbstone.pisces2.comm.Const;
import cn.bbstone.pisces2.comm.StatusEnum;
import cn.bbstone.pisces2.comm.cache.ClientFliIndexCache;
import cn.bbstone.pisces2.comm.fli.FliSavePoint;
import cn.bbstone.pisces2.config.Config;
import cn.bbstone.pisces2.proto.rsp.RspFile;
import cn.bbstone.pisces2.util.BByteUtil;
import cn.bbstone.pisces2.util.BFileUtil;
import cn.bbstone.pisces2.util.ConfigUtil;
import cn.bbstone.pisces2.util.CtxUtil;
import cn.hutool.core.util.ObjectUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class FileTask implements ITask {
    private static Logger log = LoggerFactory.getLogger(FileTask.class);

    private List<TaskListener> listener = new ArrayList<>();

    private long recvSize = 0; // recv bytes(still in sbuf)
    private long saveSize = 0; // total saved bytes(saved to disk)

    private FileOutputStream fos = null;

    // 4M -> 32M (for 40w+ small files(most file size < 1M) test)
    private int SBUF_SIZE = Const.DEFAULT_CLIENT_RECV_BUFFER_SIZE;

    // accumulate SBUF_SIZE before save file data
    private byte[] sbuf = new byte[SBUF_SIZE];

    // sbuf next write position
    private int spos = 0;

    private String clientFullPath = null;
    private String tempFullPath = null;

    private long startTime = System.currentTimeMillis();
    // 80 means 80%, 0 means 0%
    private int progress = 0;

    // recv times
    private int counter = 0;
//    private int chunks = 0;

    private RspFile rspInfo;
    private boolean isFileSkip = false;

    public FileTask(RspFile rspFile) {
        init(rspFile);
    }

    /**
     * when empty file content
     *
     * @param clientFullPath
     * @return
     */
    private boolean isSameContent(String clientFullPath) {
        boolean bool = false;
        String clientFileChecksum = BFileUtil.checksum(clientFullPath);
        String serverFileChecksum = BByteUtil.toStr(rspInfo.getBodyData());

        if (ObjectUtil.isEmpty(rspInfo)) {
            throw new RuntimeException("check file exists client fail. not found server file info");
        }
        bool = serverFileChecksum.equals(clientFileChecksum);
        return bool;
    }

    public boolean isFileSkip() {
        return isFileSkip;
    }

    public void addListner(TaskListener listner) {
        listener.add(listner);
    }


    private void init(RspFile rspInfo) {
        try {
            // set recv buffer size
            SBUF_SIZE = ConfigUtil.recvBufferSizeInt(CtxUtil.getConfigModel().getRecvBuffer());
            // keep response file info in FileTask
            this.rspInfo = rspInfo;
            //
            String rpath = ClientFliIndexCache.getClientIndexNotUpdatePos(rspInfo.getFileNo()).getRpath4Client();
            // escape path special character, have been overridden by FliIndex.getRpath()
//            String rpath = BFileUtil.escapePath(rpath0);
//            log.debug("fileNo: {}, rpath0: {}, escaped rpath: {}", rspInfo.getFileNo(), rpath0, rpath);
            this.clientFullPath = BFileUtil.getClientFullPathLocal(rpath);

            // delete ~/.fli_client/fli.idx file
            if (rspInfo.getFileNo() == 0) {
                // delete file in clientDir
                BFileUtil.deleteFullPath(clientFullPath);
            }
            // create temp file(delete exist first)
            this.tempFullPath = BFileUtil.getClientTempFileFullPath(clientFullPath);
            // delete temporary file ~/.fli_client/fli.idx.part
            if (Files.exists(Paths.get(tempFullPath))) {
                BFileUtil.deleteFullPath(tempFullPath);
            }

            // check if client file exists
            if (rspInfo.getFileNo() > 0 && Files.exists(Paths.get(this.clientFullPath))) {
                // check client file checksum diff from server file checksum
                // not overwrite
                if (!CtxUtil.getConfigModel().isOverwrite()) {
                    log.debug("overwrite=false, and file(fileNo: {}) exists, will skip.", rspInfo.getFileNo());
                    isFileSkip = true;
                    return; // skip file data recv
                }
                if (isSameContent(clientFullPath)) {
                    log.debug("overwrite=true, file(fileNo: {}) exist, and with same file content, will skip. ", rspInfo.getFileNo());
                    isFileSkip = true;
                    return; // skip file data recv
                }
                // delete file in clientDir
                BFileUtil.deleteRelClientPath(rpath);
            }

            // create temp file, and open it
            fos = new FileOutputStream(this.tempFullPath, true);
        } catch (FileNotFoundException e) {
            log.error(String.format("File Not Found. clientFullPath: %s, tempFullPath: %s", this.clientFullPath, this.tempFullPath), e);
        }
    }

    // TODO to close fos when exception occur
    public StatusEnum appendFileData(RspFile rspData) {
//    public synchronized StatusEnum appendFileData(byte[] fileData) {
        byte[] fileData = rspData.getBodyData();
        counter++;
        int recvDataLen = fileData.length;
        log.debug("appending fileNo.chunkNo/chunks: {}.{}/{}, chunk size: {}",
                rspData.getFileNo(), rspData.getChunkNo(), rspInfo.getChunks(), recvDataLen);
//        log.info("client recv current: {}/{} times. chunk size: {}", counter, rspInfo.getChunks(), recvDataLen);
        if (recvDataLen <= 0) { // recv empty file, create a file with no data
            log.warn("recv file data is 0.");
            return doAfterDataRecvComplete(rspData);
//            return StatusEnum.NO_DATA;
        }
        //
        recvSize += recvDataLen;
//        log.info("-<><><> total recv bytes: {}", recvSize);
        //
//        checkSBufSpace(recvDataLen);
        // if cache full/exceed
        if ((spos + recvDataLen) >= SBUF_SIZE) {
            byte[] sbuf2 = new byte[spos + recvDataLen];
            // copy cache data to sbuf2
            System.arraycopy(sbuf, 0, sbuf2, 0, spos); // spos ? spos + 1
            // copy new recv data to sbuf2
            System.arraycopy(fileData, 0, sbuf2, spos, recvDataLen);
//            System.arraycopy(sbuf, 0, sbuf2, 0, (spos+1));
            // write sbuf2 to disk
            saveToDisk(sbuf2);
        } else { // cache not full/exceed, can still store recv fileata
            // append data to storage buffer(sbuf)
            System.arraycopy(fileData, 0, sbuf, spos, recvDataLen);
            // increase sbuf next write pos
            spos += recvDataLen;
        }
//        log.info("now recv sbuf.len: {}", spos);
        // recv the last chunk
        if (rspData.getChunkNo() == rspInfo.getChunks()) {
            log.debug("chunkNo == chunks, all chunks({}) received.", rspInfo.getChunks());
//        if (counter == rspInfo.getChunks()) {
            return doAfterDataRecvComplete(rspData);
        }
//        log.debug("current buf.size: {}, do continue...", sbuf.length);
//        log.info("recv buf.size: {}, buf.data: {}", sbuf.length, HexUtil.encodeHexStr(sbuf, true));
        // recv one chunk, everything right, continue recv next chunk
        return StatusEnum.CONTINUE;
    }

    private StatusEnum doAfterDataRecvComplete(RspFile rspData) {
        // save the last chunk data(may not full of cache
        if (spos > 0) { // flush receive cache
            byte[] sbuf3 = new byte[spos];
            System.arraycopy(sbuf, 0, sbuf3, 0, spos);
            if (saveToDisk(sbuf3)) {
                // close output stream
                closeFos();
            }
        }

        // print log
        log.debug("COMPLETED->recv chunks: {}, server chunks: {}, chunkNo: {}, fileNo: {}", counter, rspInfo.getChunks(), counter, rspInfo.getFileNo());
        // check file integrity
        String serverFileCheckSum = BByteUtil.toStr(rspInfo.getBodyData()); // bodyData(byte[]) is checksum string bytes
        File tempFile = new File(this.tempFullPath);
//        if (rspInfo.getFileNo() > 0) {
            // BUGFIX will cause OOM
//            String fileBytesHex = BFileUtil.file2BytesHex(tempFile);
//                log.info("==== tempFile.bytes.length: {}, tempFile.bytes: {} ====", fileBytesHex.length(), fileBytesHex);
//        }
        String clientFileCheckSum = BFileUtil.checksum(tempFile);
        log.debug("tempFile.fileSize: {}, checksum: {}, path: {}", tempFile.length(), clientFileCheckSum, tempFile.getAbsolutePath());
        // saved temp file content is same as server, rename it
        if (serverFileCheckSum.equals(clientFileCheckSum)) {
            BFileUtil.renameCliTempFile(new File(this.tempFullPath), clientFullPath);
            log.debug("**********>>>temp file rename OK.<<<**********");
        } else {
            log.error("recv file checksum error.");
            log.error("fileNo: {}, server checksum: {}", rspInfo.getFileNo(), serverFileCheckSum);
            log.error("fileNo: {}, client checksum: {}", rspData.getFileNo(), clientFileCheckSum);
//            stopRun(); // no sense, fos close @Line191
            FailEvent failEvent = new FailEvent();
            failEvent.setCode("E01");
            failEvent.setFailEventData(new FailEventData(rspInfo.getFileNo(),
                            String.format("received file client checksum(%s) not same as server(%s).", clientFileCheckSum, serverFileCheckSum))
                );
            fireFailEvent(failEvent);
            return StatusEnum.ERR_SAVE_DATA;
        }
//        long costTime = (System.currentTimeMillis() - rspInfo.getReqTs()) / 1000;
        long costTime = (System.currentTimeMillis() - rspInfo.getReqTs());
        log.debug(">>client request file(fileNo: {}) transfer total cost time: {} ms.", rspInfo.getFileNo(), costTime);
        // clean up cache
        fireCompleteEvent();
        //
        return StatusEnum.COMPLETED;
    }

    private void fireFailEvent(FailEvent failEvent) {
        for (TaskListener l : listener) {
            l.onFail(failEvent);
        }
    }

    /**
     * file data recv complete
     * clean up cache & update save point file
     *
     */
    private void fireCompleteEvent() {
        FliSavePoint fliSavePoint = null;
        switch (rspInfo.getMsgType()) {
            case 0x03: // RSP_LIST_INFO (fli.idx)
                // after fli.idx file data recv complete,
                // validate client fli.idx file checksum, if pass, calculate lines count
                // fileNo always 0L
                fliSavePoint = new FliSavePoint();
//                            .checksum(rspFile.getChecksum()) // file checksum
//                            .count(rspFile.getFileNo()) // fli.idx file lines count
                fliSavePoint.setFileNo(0L); // fli.idx fileNo = 0
                fliSavePoint.setUpdateTime(System.currentTimeMillis());
                break;
            default: // RS_FILE_DATA
                fliSavePoint = new FliSavePoint();
//                            .checksum()
//                            .count()
                fliSavePoint.setFileNo(rspInfo.getFileNo());
                fliSavePoint.setUpdateTime(System.currentTimeMillis());
        }
        for (TaskListener l : listener) {
            l.onCompleted(fliSavePoint);
        }
    }

    /**
     * skip file
     * TODO only fire clean up cache, donot update savepoint file with skip file
     *
     */
    public void fireSkipEvent() {
        //remove file task of fileNo
        ClientCache.removeTask(rspInfo.getFileNo());

//        FliSavePoint fliSavePoint = FliSavePoint.builder()
////                            .checksum()
////                            .count()
//                .fileNo(rspInfo.getFileNo())
//                .updateTime(System.currentTimeMillis())
//                .build();
//        for (TaskListener l : listener) {
//            l.onCompleted(fliSavePoint);
//        }
    }

    /**
     * check if append file data will exceed sbuf capacity,
     * expand sbuf capacity for saving this appending data
     *
     * @param appendLen
     */
    private void checkSBufSpace(int appendLen) {
        if ((spos + appendLen) >= SBUF_SIZE) {
            byte[] sbuf2 = new byte[spos + appendLen];
            System.arraycopy(sbuf, 0, sbuf2, 0, (spos + 1));
            sbuf = sbuf2;
        }
    }

    private boolean saveToDisk(byte[] data) {
        try {
            fos.write(data);
            log.debug("wrote {}B data to disk OK.", data.length);
            saveSize += data.length; // TODO use dataUTF len or data len
            resetSbuf();
            return true;
        } catch (IOException e) {
            log.error("wrote data to disk error.", e);
            return false;
        }
    }

    private boolean saveToDisk() {
        try {
            // sbuf full data
            if (spos >= SBUF_SIZE) {
                fos.write(sbuf);
            } else { // sbuf not full data
                byte[] wdata = new byte[spos];
                System.arraycopy(sbuf, 0, wdata, 0, spos);
                fos.write(wdata);
            }
            log.debug("wrote data to disk ...");
            return true;
        } catch (IOException e) {
            log.error("wrote data to disk error.", e);
            return false;
        }
    }

    private void resetSbuf() {
        spos = 0;
    }

    private void closeFos() {
        try {
            fos.close();
            log.debug(">>>>>>>>>>>> successfully close fos for fileNo: {} <<<<<<<<<", rspInfo.getFileNo());
        } catch (IOException e) {
            log.error("close fileOutputStream error.", e);
        }
    }

    private void stopRun() {
//        closeFos();
        // stop running, TODO close client-server socket channel
        throw new RuntimeException(String.format("client recv cache error. recvSize: %d, saveSize: %d, chunkNo: %d", recvSize, saveSize, rspInfo.getChunkNo()));
    }

    public long costTimeMs() {
        return System.currentTimeMillis() - rspInfo.getReqTs();
    }

    public long chunks() {
        return rspInfo == null ? 0L : rspInfo.getChunks();
    }

    public long fileSize() {
        return rspInfo == null ? 0L : rspInfo.getFileSize();
    }

    public String fileFullPath() {
        return this.clientFullPath;
    }
}
